
#ifdef DARWIN
#include <mach-o/dyld.h>
#include <sys/param.h>
#endif

#include <chrono>
#include <ctype.h>
#include <fcntl.h>
#include <fstream>
#include <limits.h>
#include <iomanip>
#include <ios>
#include <iostream>
#include <random>
#include <rapidjson/document.h>
#include <signal.h>
#include <sstream>
#include <string>
#include <vector>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>

#include "dimension.hpp"
#include "file.hpp"
#include "fp_buf.hpp"
#include "layout_grid.hpp"
#include "option.hpp"
#include "track.hpp"
#include "media_probe.hpp"

#define EXTENSION_NAME "mp3"

class ffmpeg_input {
public:
    std::string filename;
    std::vector<std::string> options;
};

class video_input {
public:
    video_input() : track()
    {}
    int index = -1;
    std::string filename;
    video_track track;
    bool is_concat = false;
    std::string water_mark = "none";
};

class audio_input {
public:
    audio_input() : track() {}
    std::string filename;
    int input_id = 0;
    audio_track track;
    bool is_concat = false;
};

static int probe_streams(file& f, std::vector<video_track>& video_tracks, std::vector<audio_track>& audio_tracks);
static uint64_t read_begin_timestamp(const std::string& ts_pathname);
static std::string locate_ffmpeg();
// static std::string locate_ffprobe();
static void concat_audio_and_generate_filter(std::vector<audio_input>& inputs, layout *l);
static void concat_video_and_generate_filter(std::vector<video_input> &inputs, const dimension &dim, layout *layout);
static int call_ffmpeg(const std::string& ffmpeg, const std::vector<std::string>& options, const std::string& output_fn);
static std::string get_self_pathname();
static void write_media_info(const std::string& media_file);

static const char* short_options = "i:";
static FILE* main_fp = nullptr;
static media_probe media_pb;

void sighandler(int signo) {
    int exit_code = -1;
    if (main_fp) {
        fwrite("q", 1, 1, main_fp);
    } else {
        exit(-1);
    }
}

void ignore_signal(int signo) {
    std::cerr << "signal " << std::string(strsignal(signo)) << " ignored" << std::endl;
    return;
}

int main(int argc, char** argv) {
    //signal(SIGTERM, &sighandler);
    signal(SIGPIPE, &ignore_signal);
    options o;
    auto opt_err = o.parse(argc, argv);
    if (opt_err) {
        std::cerr << "merge: failed to parse options" << std::endl;
        return 7;
    }
    if (o.input_files.empty()) {
        std::cerr << "merge: no input file" << std::endl;
        return 8;
    }
    for (auto& in : o.input_files) {
        std::string fn(in.path_name);
        std::string ts_fn(fn);
        auto last_dot = fn.rfind(".");
        if (last_dot == std::string::npos) {
            std::cerr << "merge: input file " << fn << " without extension name" << std::endl;
            return 2;
        }
        ts_fn = ts_fn.substr(0, last_dot);
        ts_fn += ".ts";
        struct stat st;
        if (stat(fn.c_str(), &st) != 0) {
            std::cerr << "merge: input file " << fn << " not found" << std::endl;
            return 1;
        }
        if (stat(ts_fn.c_str(), &st) != 0) {
            std::cerr << "merge: input file " << fn << " without ts file" << std::endl;
            return 3;
        }

        // if begin_ts is 0, we should fix it by read other files
        // this must be done before we do layout
        in.begin_ts = read_begin_timestamp(ts_fn);
    }

    layout* l = new layout_grid();
    std::cout << "merge: probe raw input files" << std::endl;
    std::vector<ffmpeg_input> ffmpeg_inputs;
    std::vector<video_input> video_inputs;
    std::vector<audio_input> audio_inputs;
    dimension max_dim(0, 0);
    bool last_file_is_concat = false;
    uint64_t last_file_end_ts = 0;
    int input_index = 0;

    auto curMark = o.water_marks.begin();
    for (auto& f : o.input_files) {
        std::vector<video_track> video_tracks;
        std::vector<audio_track> audio_tracks;
        auto probe_err = probe_streams(f, video_tracks, audio_tracks);
        if (probe_err) {
            return probe_err;
        }
        // when concating two inputs
        // the second file's begin time should not be smaller than first file's end time
        if (f.begin_ts < last_file_end_ts && last_file_is_concat) {
            f.begin_ts = last_file_end_ts;
            for (auto& v : video_tracks) {
                v.begin_ts = last_file_end_ts;
            }
            for (auto& a : audio_tracks) {
                a.begin_ts = last_file_end_ts;
            }
            std::cout << "begin ts overlap adjusted for file " << f.path_name << std::endl;
        }
        last_file_end_ts = f.begin_ts + f.duration_ms;
        last_file_is_concat = f.is_concat;
        if (audio_tracks.size() && !o.is_audio_disabled) {
            if (audio_tracks.size() > 1) {
                std::cout << "file " << f.path_name << "has more than one audio track, only the first one is used" << std::endl;
                while (audio_tracks.size() > 1) {
                    audio_tracks.pop_back();
                }
            }
            // l->add_audio(ffmpeg_inputs.size(), audio_tracks.front());
            audio_input ain;
            ain.filename = f.path_name;
            ain.track = audio_tracks.front();
            ain.is_concat = f.is_concat;
            // ain.input_id = static_cast<int>(ffmpeg_inputs.size());
            ain.input_id = input_index;
            audio_inputs.emplace_back(ain);
        }
        if (video_tracks.size() && !o.is_video_disabled) {
            if (video_tracks.size() > 1) {
                std::cout << "file " << f.path_name << " has more than one video tracks, only first is used" << std::endl;
                while (video_tracks.size() > 1) {
                    video_tracks.pop_back();
                }
            }

            for (auto& v : video_tracks) {
                if (v.dim.w > max_dim.w) {
                    max_dim.w = v.dim.w;
                }
                if (v.dim.h > max_dim.h) {
                    max_dim.h = v.dim.h;
                }
                video_input vin;
                vin.filename = f.path_name;
                vin.track = video_tracks.front();
                vin.is_concat = f.is_concat;
                vin.water_mark = *curMark;
                vin.index = input_index;
                video_inputs.emplace_back(vin);
            }
        }

        curMark++;
        ffmpeg_inputs.emplace_back(ffmpeg_input{.filename = f.path_name, .options = {}});
        ++input_index;
    }

    if (audio_inputs.size() && !o.is_audio_disabled) {
        std::vector<audio_input> input_cache;
        for (size_t i = 0; i < audio_inputs.size(); ++i) {
            input_cache.emplace_back(audio_inputs[i]);
            if (i != audio_inputs.size() - 1 && audio_inputs[i].is_concat == true) {
                continue;
            }
            concat_audio_and_generate_filter(input_cache, l);
            input_cache.clear();
        }
    }

    if (video_inputs.size() && !o.is_video_disabled) {
        // split inputs into groups, concat input in same group
        std::vector<video_input> input_cache;
//        std::string output_dir = o.output_filename;
//        auto last_slash = output_dir.rfind("/");
//        output_dir = output_dir.substr(0, last_slash);
        for (size_t i = 0; i < video_inputs.size(); ++i) {
            input_cache.emplace_back(video_inputs[i]);
            if (i != video_inputs.size() - 1 && video_inputs[i].is_concat == true) {
                continue;
            }
            concat_video_and_generate_filter(input_cache, max_dim, l);
            input_cache.clear();
            //std::ostringstream size;
            //l->add_video(ffmpeg_inputs.size(), norm_output.track);
            //size << max_dim.w << "x" << max_dim.h;
            //ffmpeg_inputs.emplace_back(ffmpeg_input{.filename = norm_output.filename, .options = {"-f", "rawvideo", "-s:v", size.str(), "-r", "15"}});

        }
    }

    std::string ffmpeg = locate_ffmpeg();
    if (ffmpeg.empty()) {
        std::cerr << "merge: can't find ffmpeg, it should be in same path to merge, or defined by environment variable FFMPEG" << std::endl;
        return 10;
    }
    std::vector<std::string> args;
    bool no_video = false;
    args.emplace_back("-y");
    for (auto go : o.global_options) {
        args.emplace_back(go);
    }
    for (auto i : ffmpeg_inputs) {
        for (auto& io : i.options) {
            args.emplace_back(io);
        }
        
        args.emplace_back("-i");
        args.emplace_back(i.filename);
    }

    std::vector<std::string> video_output_options;
    if (!o.is_video_disabled) {
        l->video_output_options(video_output_options);
        for (auto voo : video_output_options) {
            args.emplace_back(voo);
        }

        if(!l->has_video()) {
            no_video = true;
        }
    }

    std::vector<std::string> audio_output_options;
    if (!o.is_audio_disabled) {
        l->audio_output_options(audio_output_options);
        for (auto aoo : audio_output_options) {
            args.emplace_back(aoo);
        }
    }

    if(no_video) {
        //change output file
        std::string output_file = o.output_filename;
        auto last_slash = output_file.rfind(".");
        if (last_slash != std::string::npos) {
            output_file = output_file.substr(0, last_slash + 1);
        }
        output_file += EXTENSION_NAME;
        o.output_filename = output_file;

        //change options arg
        //-c:a aac --> -c:a libmp3lame
        // std::string out_options = o.output_options;
        bool has_audio_codec = false;
        for(auto& i : o.output_options) {
            if("-c:a" == i) {
                has_audio_codec = true;
                continue;
            }
            if(has_audio_codec) {
                i = "libmp3lame";
                break;
            }
        }

        if(!has_audio_codec) {
            o.output_options.emplace_back("-c:a");
            o.output_options.emplace_back("libmp3lame");
        }
    }

    for (auto& oo : o.output_options) {
        args.emplace_back(oo);
    }

    args.emplace_back(o.output_filename);

    std::cout << "merge: begin" << std::endl;
    std::cout << ffmpeg;
    for (auto s : args) {
        std::cout << " " << s;
    }
    std::cout << std::endl;
    int err = call_ffmpeg(ffmpeg, args, o.output_filename);
    for (auto& i : ffmpeg_inputs) {
        if (i.filename.find(".norm.yuv") != std::string::npos) {
            unlink(i.filename.c_str());
        }
    }
    if (err) {
        std::cerr << "merge: error occured" << std::endl;
    }

    write_media_info(o.output_filename);
    return err;
}

static void write_media_info(const std::string& media_file) {
    if(media_file.empty()) {
        return ;
    }

	std::string::size_type p1 = media_file.find_last_of('/');
	std::string::size_type p2 = media_file.find_last_of('.');
	if (p1 == std::string::npos || p2 == std::string::npos) {
		exit(-1);
	}
	std::string rtcName = media_file.substr(p1 + 1, p2 - p1 - 1);
	std::string writePath = media_file.substr(0, p1 + 1);
    media_pb.writeMediaInfoToFile(media_file, rtcName, writePath);
    std::cout << "ffprobe.json file = " << writePath  + rtcName + ".ffprobe.json" << std::endl;
}

static uint64_t read_begin_timestamp(const std::string& ts_pathname) {
    std::cout << "read ts: file=" << ts_pathname << std::endl;
    static const std::string key_begin = "begin";
    std::ifstream reader(ts_pathname, std::ios::in);
    if (!reader.is_open()) {
        std::cerr << "read ts: can't open ts file " << std::endl;
        return 0;
    }

    for (std::string line; std::getline(reader, line);) {
        auto eq = line.find("=");
        if (eq == std::string::npos) {
            continue;
        }
        std::string key = line.substr(0, eq);
        std::string value = line.substr(eq + 1, line.length() - eq - 1);
        if (key == key_begin) {
            std::cout << "read ts: ts=" << value << std::endl;
            return strtoull(value.c_str(), nullptr, 10);
        }
    }
    std::cerr << "read ts: can't read begin timestamp" << std::endl;
    return 0;
}

static int probe_streams(file& f, std::vector<video_track>& video_tracks, std::vector<audio_track>& audio_tracks) {
    // std::cout << "probe: file: " << f.path_name << std::endl;
    // std::string ffprobe = locate_ffprobe();
    // if (ffprobe.empty()) {
    //     std::cerr << "probe: can't find ffprobe, it should be in same path to merge, or you can define environment FFPROBE to override this rule" << std::endl;
    //     exit(-1);
    // }

    // std::ostringstream w;
    // w << ffprobe << " -hide_banner " << f.path_name << " -show_entries format=duration:stream=index,codec_type,width,height,sample_rate,channels,channel_layout -of json=compact=1";
    // std::cout << w.str() << std::endl;
    // fp_buf fb(popen(w.str().c_str(), "r"));
    // std::istream in(&fb);
    // std::string ffprobe_json_string;
    // for (std::string line; std::getline(in, line); ) {
    //     ffprobe_json_string += line;
    // }

    std::cout << " >> begin probe media: " << f.path_name << std::endl;
    const std::string& ffprobe_json_string = media_pb.readMediaInfo2Json(f.path_name);
    if(ffprobe_json_string == "N/A" || ffprobe_json_string.empty()) {
        std::cerr << "probe: can't find ffprobe, it should be in same path to merge, or you can define environment FFPROBE to override this rule" << std::endl;
        exit(-1);
    }

    rapidjson::Document ffprobe_json;
    ffprobe_json.Parse(ffprobe_json_string);
    if (!ffprobe_json.IsObject()) {
        std::cerr << "probe: error reading ffprobe output: " << ffprobe_json_string << std::endl;
        return 1;
    }
    rapidjson::Value::ConstMemberIterator streams = ffprobe_json.FindMember("streams");
    rapidjson::Value::ConstMemberIterator format = ffprobe_json.FindMember("format");
    if (streams == ffprobe_json.MemberEnd() || !streams->value.IsArray()) {
        std::cerr << "probe: error reading streams" << std::endl;
        return 2;
    }
    if (format == ffprobe_json.MemberEnd() || !format->value.IsObject()) {
        std::cerr << "probe: error reading format" << std::endl;
        return 3;
    }
    auto format_object = format->value.GetObject();
    rapidjson::Value::ConstMemberIterator duration = format_object.FindMember("duration");
    if (duration == format_object.MemberEnd() || !duration->value.IsString()) {
        std::cerr << "probe: error reading format.duration" << std::endl;
        return 4;
    }
    std::string duration_string(duration->value.GetString(), duration->value.GetStringLength());
    f.duration_ms = strtof(duration_string.c_str(), nullptr) * 1000;
    auto streams_array = streams->value.GetArray();
    for (rapidjson::Value::ConstValueIterator stream = streams_array.Begin();
            stream != streams_array.End(); ++stream) {
        if (!stream->IsObject()) {
            continue;
        }
        auto stream_object = stream->GetObject();
        rapidjson::Value::ConstMemberIterator stream_codec_type = stream_object.FindMember("codec_type");
        rapidjson::Value::ConstMemberIterator stream_index = stream_object.FindMember("index");
        if (stream_codec_type == stream_object.MemberEnd() || !stream_codec_type->value.IsString()) {
            std::cerr << "probe: error reading stream.codec_type" << std::endl;
            continue;
        }
        std::string stream_codec_type_string(stream_codec_type->value.GetString(),  stream_codec_type->value.GetStringLength());
        if (stream_index == stream_object.MemberEnd() || !stream_index->value.IsInt()) {
            std::cerr << "probe: error reading stream.index" << std::endl;
            continue;
        }
        auto stream_index_int = stream_index->value.GetInt();
        if (stream_codec_type_string == "video") {
            rapidjson::Value::ConstMemberIterator stream_width = stream_object.FindMember("width");
            rapidjson::Value::ConstMemberIterator stream_height = stream_object.FindMember("height");
            if (stream_width == stream_object.MemberEnd() || !stream_width->value.IsInt()) {
                std::cerr << "probe: error reading stream.width" << std::endl;
                continue;
            }
            auto stream_width_int = stream_width->value.GetInt();
            if (stream_height == stream_object.MemberEnd() || !stream_height->value.IsInt()) {
                std::cerr << "probe: error reading stream.height" << std::endl;
                continue;
            }
            auto stream_height_int = stream_height->value.GetInt();
            video_tracks.emplace_back(video_track(f, stream_index_int, dimension(stream_width_int, stream_height_int)));
            std::cout << "probe: stream: " << stream_index_int << " video: " << stream_width_int << "x" << stream_height_int << std::endl;
        } else if (stream_codec_type_string == "audio") {
            rapidjson::Value::ConstMemberIterator stream_sample_rate = stream_object.FindMember("sample_rate");
            rapidjson::Value::ConstMemberIterator stream_channels = stream_object.FindMember("channels");
            rapidjson::Value::ConstMemberIterator stream_channel_layout = stream_object.FindMember("channel_layout");
            if (stream_sample_rate == stream_object.MemberEnd() || !stream_sample_rate->value.IsString()) {
                std::cerr << "probe: error reading stream.sample_rate" << std::endl;
                continue;
            }
            if (stream_channels == stream_object.MemberEnd() || !stream_channels->value.IsInt()) {
                std::cerr << "probe: error reading stream.channels" << std::endl;
                continue;
            }
            // if (stream_channel_layout == stream_object.MemberEnd() || !stream_channel_layout->value.IsString()) {
            //     std::cerr << "probe: error reading stream.channel_layout" << std::endl;
            //     continue;
            // }
            int sample_rate = atoi(std::string(stream_sample_rate->value.GetString(), stream_sample_rate->value.GetStringLength()).c_str());
            int channels = stream_channels->value.GetInt();
            // std::string channel_layout(stream_channel_layout->value.GetString(), stream_channel_layout->value.GetStringLength());
            audio_tracks.emplace_back(audio_track(f, stream_index_int, sample_rate, channels));
            // std::cout << "probe: stream: " << stream_index_int << " audio: sample_rate=" << sample_rate << " channels=" << channels << " layout=" << channel_layout << std::endl;
            std::cout << "probe: stream: " << stream_index_int << " audio: sample_rate=" << sample_rate << " channels=" << channels << std::endl;
        }
    }

    return 0;
}

static std::string locate_ffmpeg() {
    char* ffmpeg = getenv("FFMPEG");
    if (ffmpeg != nullptr) {
        struct stat stat_buf;
        int stat_error = stat(ffmpeg, &stat_buf);
        if (stat_error == 0) {
            return std::string(ffmpeg);
        }
    }
    // std::string merge_path = get_self_pathname();
    // auto last_slash = merge_path.rfind("/");
    // if (last_slash != std::string::npos) {
    //     merge_path = merge_path.substr(0, last_slash + 1);
    // }
    // merge_path += "ffmpeg";
    return "/usr/bin/ffmpeg";
}

// static std::string locate_ffprobe() {
//     char* ffprobe = getenv("FFPROBE");
//     if (ffprobe != nullptr) {
//         struct stat stat_buf;
//         int stat_error = stat(ffprobe, &stat_buf);
//         if (stat_error == 0) {
//             return std::string(ffprobe);
//         }
//     }
//     // std::string merge_path = get_self_pathname();
//     // std::cout << "merge_path=" << merge_path << std::endl;
//     // auto last_slash = merge_path.rfind("/");
//     // if (last_slash != std::string::npos) {
//     //     merge_path = merge_path.substr(0, last_slash + 1);
//     // }
//     // merge_path += "ffprobe";

//     return "/usr/bin/ffprobe";
// }

static
std::string generate_id(int digits){
    static std::string tab = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    static std::random_device r;
    std::ostringstream w;
    while (digits--) {
        w << tab.substr(r()%52,1);
    }
    return w.str();
}

static int _mkdir_tree(const char *path) {
    char tmp[1024] = {0,};
    char *p = nullptr;
    snprintf(tmp, sizeof(tmp), "%s", path);
    size_t len = strlen(tmp);
    if (tmp[len - 1] == '/') {
        tmp[len - 1] = 0;
    }
    for (p = tmp + 1; *p; p++) {
        if(*p == '/') {
            *p = 0;
            mkdir(tmp, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
            *p = '/';
        }
    }
    mkdir(tmp, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
    return 0;
}

static void concat_audio_and_generate_filter(std::vector<audio_input>& inputs, layout *l) {
    static int source_id = -1;
    ++source_id;
    audio_filter_track a;
    int segment_id = -1;
    int gap_id = -1;
    uint64_t last_end_ts = 0;
    std::ostringstream filter;
    std::ostringstream concat_inputs;
    int concat_n = 0;
    for (auto& input : inputs) {
        segment_id++;
        std::cout << "concat audio source " << source_id << " segment " << segment_id << "/" << inputs.size() << std::endl;
        std::cout << "  input: " << input.filename << std::endl;
        std::cout << "  begin: " << input.track.begin_ts << std::endl;
        std::cout << "  duration: " << input.track.duration_ms / 1000 << "." << input.track.duration_ms % 1000 << "s" << std::endl;
        std::cout << "  sample_rate: " << input.track.sample_rate << std::endl;
        std::cout << "  channel_nb: " << input.track.channels << std::endl;
        if (last_end_ts != 0 && input.track.begin_ts > last_end_ts) {
            gap_id++;
            std::ostringstream value_list; value_list << 0;
            for (int i = 1; i < input.track.channels; ++i) {
                value_list << "|" << 0;
            }
            uint64_t gap_ms = input.track.begin_ts - last_end_ts;
            filter << "aevalsrc=" << value_list.str() << ":d=" << gap_ms / 1000 << "." << gap_ms % 1000;
            filter << "[a" << source_id << "-g" << gap_id << "];";
            concat_inputs << "[a" << source_id << "-g" << gap_id << "]";
            concat_n++;
            a.duration_ms += gap_ms;
        }
        last_end_ts = input.track.begin_ts + input.track.duration_ms;
        filter << "[" << input.input_id << ":a]";
        filter << "aresample=" << input.track.sample_rate << ":async=1,afifo";
        filter << "[a" << source_id << "-s" << segment_id << "];";
        concat_inputs << "[a" << source_id << "-s" << segment_id << "]";
        concat_n++;
        if (a.begin_ts == 0) {
            a.begin_ts = input.track.begin_ts;
        }
        a.duration_ms += input.track.duration_ms;
    }
    if (inputs.size() == 1) {
        filter << concat_inputs.str() << "anull";
    } else {
        filter << concat_inputs.str();
        filter << "concat=n=" << concat_n << ":v=0:a=1,afifo";
    }

    a.filter = filter.str();
    std::cout << "  filter: " << a.filter << std::endl;
    l->add_audio(a);
}

static void concat_video_and_generate_filter(std::vector<video_input> &inputs, const dimension &dim, layout *l) {
    // s1 for each normalize video
    static int s1 = -1;
    ++s1;
    video_filter_track t;
    // using first file's first track's begin ts
    t.begin_ts = inputs.begin()->track.begin_ts;
    t.duration_ms = 0;
    t.dim = dim;

    int in_cnt = 0;
    for (auto& i : inputs) {
        ++in_cnt;
        std::cout << "normalize input " << in_cnt << "/" << inputs.size() << std::endl;
        std::cout << "  input: " << i.filename << std::endl;
        std::cout << "  begin_ts: " << i.track.begin_ts << std::endl;
        std::cout << "  duration: " << i.track.duration_ms / 1000 << "." << i.track.duration_ms % 1000 << "s" << std::endl;
        std::cout << "  dimension: " << i.track.dim.w << "x" << i.track.dim.h << " -> " << dim.w << "x" << dim.h << std::endl;
    }
    std::ostringstream filter_string;
    auto i = inputs.begin();
    video_track& v = i->track;
    // s2 is for each parts of concat
    int s2 = 0;
//    filter_string << "color=color=Black:size=" << dim.w << "x" << dim.h << ":d=" << v.duration_ms/1000 << "." << v.duration_ms%1000;
//    filter_string << ",format=pix_fmts=yuv420p[n" << s1 << "-" << s2 << "-bg];";
//    filter_string << "movie=" << i->filename << ":stream_index=" << v.id;
//    filter_string << ",scale=w='iw*min(" << dim.w << "/iw," << dim.h << "/ih)':h='ih*min(" << dim.w << "/iw," << dim.h << "/ih)'[n" << s1 << "-" << s2 << "-fg];";
//    filter_string << "[n" << s1 << "-" << s2 << "-bg][n" << s1 << "-" << s2 << "-fg]" "overlay=x=(W-w)/2:y=(H-h)/2[n" << s1 << "-" << s2 << "]";
//    uint64_t last_end_ts = v.begin_ts + v.duration_ms;
//    i++;
//    s2++;
    uint64_t last_end_ts = (uint64_t)-1;
    while(i != inputs.end()) {
//        filter_string << ";";
        v = i->track;
        if (v.begin_ts >= last_end_ts) {
            uint64_t gap_ms = v.begin_ts - last_end_ts;
            filter_string << "color=color=Black:size=" << dim.w << "x" << dim.h << ":d=" << gap_ms/1000 << "." << gap_ms%1000;
            filter_string << ",format=pix_fmts=yuv420p[n" << s1 << "-" << s2 << "];";
            s2++;
            t.duration_ms += gap_ms;
        }
        filter_string << "color=color=Black:size=" << dim.w << "x" << dim.h << ":d=" << v.duration_ms/1000 << "." << v.duration_ms%1000;
        filter_string << ",format=pix_fmts=yuv420p[n" << s1 << "-" << s2 << "-bg];";
        filter_string << "movie=" << i->filename << ":stream_index=" << v.id << ",";
        // filter_string << "[" << i->index << ":v]";
        filter_string << "scale=w='iw*min(" << dim.w << "/iw," << dim.h << "/ih)':h='ih*min(" << dim.w << "/iw," << dim.h << "/ih)'[n" << s1 << "-" << s2 << "-fg];";
        if("none" == i->water_mark){ //没有水印
            filter_string << "[n" << s1 << "-" << s2 << "-bg][n" << s1 << "-" << s2 << "-fg]" "overlay=x=(W-w)/2:y=(H-h)/2[n" << s1 << "-" << s2 << "];";
        }else{ //--water-mark movie='/Users/yangss/Downloads/xswitch_samples/logo.png':stream_index=0;overlay=main_w-overlay_w-5:5
            filter_string << "[n" << s1 << "-" << s2 << "-bg][n" << s1 << "-" << s2 << "-fg]" "overlay=x=(W-w)/2:y=(H-h)/2[n" << s1 << "-" << s2 << "-pre-mark];";
            auto semicolonIndex = i->water_mark.find(';');
            filter_string << i->water_mark.substr(0, semicolonIndex) << "[n" << s1 << "-" << s2 << "-water-mark];";
            filter_string << "[n" << s1 << "-" << s2 << "-pre-mark]" << "[n" << s1 << "-" << s2 << "-water-mark]" << i->water_mark.substr(semicolonIndex+1) << "[n" << s1 << "-" << s2 <<"];";
        }

        last_end_ts = v.begin_ts + v.duration_ms;
        i++;
        s2++;
        t.duration_ms += v.duration_ms;
    }
    for (int si = 0; si < s2; si++) {
        filter_string << "[n" << s1 << "-" << si << "]";
    }
    filter_string << "concat=n=" << s2 << ":v=1:a=0";
    t.filter = filter_string.str();
    std::cout << "  filter: " << t.filter << std::endl;
    l->add_video(t);
    return;
}

static int call_ffmpeg(const std::string& ffmpeg, const std::vector<std::string>& options, const std::string& output_fn) {
    std::string progress_fn = output_fn + ".progress";
    unlink(progress_fn.c_str());
    int err = mkfifo(progress_fn.c_str(), S_IRWXU|S_IRWXG|S_IRWXO);
    if (err) {
        std::cerr << "create fifo failed" << std::endl;
        return -5;
    }
    int pipe_downward[2];
    //int pipe_upward[2];

    int pipe_err = pipe(pipe_downward);
    if (pipe_err) {
        std::cout << "create downward pipe failed" << std::endl;
        unlink(progress_fn.c_str());
        return -10;
    }
    int& parent_write = pipe_downward[1];
    int& child_read = pipe_downward[0];
    std::cout << "parent write: " << parent_write << std::endl;
    std::cout << "child read: " << child_read << std::endl;
    pid_t pid = fork();
    if (pid == 0) {
        // child
        // caveats: if parent have stdin/out closed, parent read could be 0, then following code attachs child read to parent_read, this will get child killed
        // for defails, see http://unixwiz.net/techtips/remap-pipe-fds.html
        // close(parent_read);
        close(parent_write);
        dup2(child_read, 0);
        int null_fd = open("/dev/null", O_WRONLY);
        // uncomment to ignore ffmpeg output
        //dup2(null_fd, 1);
        //dup2(null_fd, 2);
        close(child_read);
        close(null_fd);

        std::string ffmpeg = locate_ffmpeg();
        int argc = options.size() + 3;
        if (argc > 512) {
            std::cerr << "too many args" << std::endl;
            return -20;
        }
        char* argv[512];
        char* argv_buf = (char*)malloc(1024 * 1024);
        char* argv_buf_ptr = argv_buf;
        int i = 0;
        argv[i] = argv_buf_ptr;
        memcpy(argv_buf_ptr, ffmpeg.c_str(), ffmpeg.length());
        argv_buf_ptr[ffmpeg.length()] = '\0';
        argv_buf_ptr += ffmpeg.length() + 1;
        i += 1;
        for (auto& o : options) {
            argv[i] = argv_buf_ptr;
            memcpy(argv_buf_ptr, o.c_str(), o.length());
            argv_buf_ptr[o.size()] = '\0';
            argv_buf_ptr += o.size() + 1;
            i += 1;
        }

        // argv[i] = argv_buf_ptr;
        // memcpy(argv_buf_ptr, "-progress", strlen("-progress"));
        // argv_buf_ptr[strlen("-progress")] = '\0';
        // argv_buf_ptr += strlen("-progress") + 1;
        // i += 1;

        // argv[i] = argv_buf_ptr;
        // memcpy(argv_buf_ptr, progress_fn.c_str(), progress_fn.length());
        // argv_buf_ptr[progress_fn.length()] = '\0';
        // argv_buf_ptr += progress_fn.length() + 1;
        // i += 1;

        argv[i] = nullptr;

        // std::cout << "ffmpeg ";
        // for(int j = 0; j < i; ++j) {
        //     std::cout << argv[j] << " ";
        // }
        // std::cout << std::endl;
        int err = execv(ffmpeg.c_str(), argv);
        free(argv_buf);
        if (err) {
            std::cerr << "execv failed" << std::endl;
            _exit(-25);
        }
        _exit(0);
    } else if (pid < 0) {
        std::cout << "fork failed" << std::endl;
        return -30;
    } else {
        // parent
        int return_code;
        int fd;
        do {
            close(child_read);
            //close(child_write);
            pid_t child_pid = pid;
            fd = open(progress_fn.c_str(), O_RDONLY | O_NONBLOCK);
            if (fd < 0) {
                std::cout << "open the pipe failed" << std::endl;
                return_code = -35;
                break;
            }

            char buf[4096];
            int buf_size = 0;
            int last_frame = 0;
            int last_frame_repeated = -1;
            while (true) {
                // todo: handle buffer full
                int bytes = read(fd, buf + buf_size, 4096 - buf_size);
                if (bytes == 0) {
                    // remote end closed
                } else if (bytes > 0) {
                    //std::cout << "progress read: " << bytes << std::endl;
                    buf_size += bytes;
                } else if (errno == EAGAIN || errno == EWOULDBLOCK) {

                }
                std::string frame;
                std::string speed;
                char* buf_ptr = buf;
                while (buf_ptr < buf + buf_size) {
                    char* line_end = strchr(buf_ptr, '\n');
                    if (line_end) {
                        char* separator = strstr(buf_ptr, "=");
                        if (separator && separator < line_end) {
                            std::string key(buf_ptr, separator - buf_ptr);
                            std::string value(separator + 1, line_end - separator - 1);
                            //std::cout << "progress: " << key << " " << value << std::endl;
                            if (key == "frame") {
                                frame = value;
                            } else if (key == "speed") {
                                speed = value;
                            }
                        }
                        buf_ptr = line_end + 1;
                    } else {
                        break;
                    }
                }
                if (frame.size()) {
                    std::cout << progress_fn << ": frame=" << frame << " speed=" << speed << " buf_size=" << buf_size << std::endl;
                    int frame_i = atoi(frame.c_str());
                    if (frame_i == last_frame) {
                        last_frame_repeated += 1;
                        std::cerr << progress_fn << ": last frame repeated " << last_frame_repeated << " times" << std::endl;
                    } else {
                        last_frame = frame_i;
                        last_frame_repeated = 0;
                    }
                    if (last_frame_repeated >= 5) {
                        write(parent_write, "q", 1);
                        return_code = -40;
                        break;
                    }
                }
                if (buf_ptr < buf + buf_size) {
                    buf_size = buf + buf_size - buf_ptr;
                    memmove(buf, buf_ptr, buf_size);
                } else {
                    buf_size = 0;
                }


                int wait_status;
                pid_t report_pid = waitpid(child_pid, &wait_status, WNOHANG|WUNTRACED);
                if (report_pid == -1) {
                    if (errno == ECHILD) {
                        std::cout << "waitpid failed: no such child" << std::endl;
                        return_code = -45;
                        break;
                    } else if (errno == EINTR) {
                        std::cout << "waitpid interrupted: kill child" << std::endl;
                        write(parent_write, "q", 1);
                        return_code = -50;
                        break;
                    } else if (errno == EINVAL) {
                        std::cout << "waitpid failed: invalid option" << std::endl;
                        write(parent_write, "q", 1);
                        return_code = -55;
                        break;
                    }
                } else if (report_pid == child_pid) {
                    // child exit
                    return_code = -60;
                    if (WIFEXITED(wait_status)) {
                        return_code = WEXITSTATUS(wait_status);
                    }
                    std::cout << "child exit: " << return_code << std::endl;
                    break;
                }
                std::this_thread::sleep_for(std::chrono::milliseconds(250));
            }
        } while(false);
        if (parent_write >= 0) {
            close(parent_write);
        }
        if (fd >= 0) {
            close(fd);
        }

        unlink(progress_fn.c_str());
        return return_code;
    } // end of parent
}

#if DARWIN
std::string get_self_pathname() {
    char path[MAXPATHLEN];
    uint32_t size = sizeof(path);
    if (_NSGetExecutablePath(path, &size) == 0) {
        return std::string(path);
    }
    return std::string();
}
#endif

#if LINUX
std::string get_self_pathname() {
    char bbb[1024];
    char path[PATH_MAX];
    int bytes = readlink("/proc/self/exe", path, PATH_MAX);
    //std::cout << "readlink(/proc/self/exe) returns " << bytes << std::endl;
    if (bytes > 0) {
        //std::cout << "readlink " << bytes << std::endl;
        return std::string(path, bytes);
    } else {
        std::cout << "readlink err " << errno << std::endl;
        return std::string(realpath(".", path));
    }
    return std::string();
};
#endif